package org.jetlinks.pro.assets.impl;

import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.NonNull;
import org.hswebframework.ezorm.core.Conditional;
import org.hswebframework.ezorm.core.NestConditional;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.Dimension;
import org.hswebframework.web.authorization.DimensionType;
import org.jetlinks.pro.assets.*;
import org.jetlinks.pro.assets.impl.terms.DimensionAssetsTerm;
import org.jetlinks.pro.assets.impl.terms.DimensionAssetsTermBuilder;
import org.jetlinks.pro.topic.Topics;
import org.springframework.core.Ordered;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 基于维度的资产权限持有者,通过获取权限信息{@link org.hswebframework.web.authorization.Authentication}
 * 中的维度信息{@link Authentication#getDimensions()}来进行权限控制.
 * <p>
 * 可以通过{@link org.hswebframework.web.authorization.DimensionProvider}来定义用户维度关联信息.
 * <p>
 * 维度类型:
 * {@link Dimension#getType()} ()}等同于{@link AssetBinding#getTargetType()},
 * <p>
 * 维度ID:
 * {@link Dimension#getId()}等同于{@link AssetBinding#getTargetId()},
 *
 * @author zhouhao
 * @see AssetsHolder
 * @see Dimension
 * @see DimensionAssetBindManager
 * @since 1.11
 */
@AllArgsConstructor
class DimensionAssetsHolder implements AssetsHolder, Ordered {

    //维度信息
    final List<Dimension> dimensions;

    //资产管理器
    private final AssetManager assetManager;

    //资产绑定管理器
    private final AssetBindManager bindManager;

    //是否自动绑定
    private final boolean autoBinding;

    //预留
    private final String userId;

    //忽略处理的资产类型
    private final Set<String> excludeAssetTypes;

    @Override
    public Flux<DimensionBinding> getDimensionBindings() {
        return Flux
            .fromIterable(dimensions)
            .map(SimpleDimensionBinding::new);
    }

    @Override
    public Flux<DimensionBinding> getDimensionBindings(DimensionType targetType) {
        return Flux
            .fromIterable(dimensions)
            .filter(dimension -> dimension.typeIs(targetType))
            .map(SimpleDimensionBinding::new);
    }

    @Override
    public Flux<String> refactorTopic(String topic) {
        return getDimensionBindings()
            /*  /{targetType}/{targetId}/{topic}  */
            .map(binding -> Topics.binding(binding.getTargetType().getId(), binding.getTargetId(), topic));
    }

    /**
     * 获取维度信息,并按维度类型分组,返回二维组[ 维度类型ID,List[维度ID] ]
     *
     * @param filter 维度过滤器
     * @return 维度信息
     */
    private Flux<Tuple2<String, List<String>>> getDimensions(Predicate<Dimension> filter) {
        return Flux
            .fromIterable(dimensions)
            .filter(filter)
            .groupBy(dimension -> dimension.getType().getId())
            .flatMap(group -> Mono
                .zip(
                    Mono.just(String.valueOf(group.key())),
                    group.map(Dimension::getId).collectList()
                ));
    }

    private boolean isExclude(AssetType type) {
        return isExclude(type.getId());
    }

    private boolean isExclude(String type) {
        return excludeAssetTypes != null && excludeAssetTypes.contains(type);
    }

    @Override
    public Flux<AssetBinding> getBindings(AssetType assetType) {
        if (isExclude(assetType)) {
            return Flux.empty();
        }
        return this
            //获取指定维度
            .getDimensions(dimension -> true)
            //获取绑定关系
            .flatMap(tp2 -> bindManager.getBindings(tp2.getT1(), tp2.getT2(), assetType));
    }

    @Override
    public Flux<AssetBinding> getBindings(AssetType assetType,
                                          Collection<?> idList) {
        if (isExclude(assetType)) {
            return Flux.empty();
        }
        return this
            //获取指定维度
            .getDimensions(dimension -> true)
            //获取绑定关系
            .flatMap(tp2 -> bindManager.getBindings(tp2.getT1(), tp2.getT2(), assetType, idList));
    }

    @Override
    public Flux<AssetBinding> getBindings(DimensionType targetType,
                                          AssetType assetType) {
        if (isExclude(assetType)) {
            return Flux.empty();
        }
        return this
            //获取指定维度
            .getDimensions(dimension -> dimension.typeIs(targetType))
            //获取绑定关系
            .flatMap(tp2 -> bindManager.getBindings(tp2.getT1(), tp2.getT2(), assetType));
    }

    @Override
    public Flux<AssetBinding> getBindings(DimensionType targetType,
                                          Collection<?> targetIdList,
                                          AssetType assetType) {
        if (isExclude(assetType)) {
            return Flux.empty();
        }
        return this
            //获取指定维度
            .getDimensions(dimension -> dimension.typeIs(targetType) && targetIdList.contains(dimension.getId()))
            //获取绑定关系
            .flatMap(tp2 -> bindManager.getBindings(tp2.getT1(), tp2.getT2(), assetType));
    }

    @Override
    public Flux<AssetBinding> getBindings(DimensionType targetType,
                                          Collection<?> targetIdList,
                                          AssetType assetType,
                                          Collection<?> assetIdList) {
        if (isExclude(assetType)) {
            return Flux.empty();
        }
        return this
            //获取指定维度
            .getDimensions(dimension -> dimension.typeIs(targetType) && targetIdList.contains(dimension.getId()))
            //获取绑定关系
            .flatMap(tp2 -> bindManager.getBindings(tp2.getT1(), tp2.getT2(), assetType, assetIdList));
    }

    @Nonnull
    @Override
    public Mono<Void> bindAssets(@NonNull DimensionType targetType,
                                 @NonNull AssetType assetType,
                                 @NonNull Collection<?> assetId) {
        //没有配置自动绑定或者忽略处理的资产类型
        if (!autoBinding || isExclude(assetType)) {
            return Mono.empty();
        }
        return Flux
            .fromIterable(dimensions)
            .filter(dimension -> dimension.typeIs(targetType))
            //绑定到全部维度
            .map(dimension -> createAssetBindRequest(dimension, assetType.getId(), assetId))
            .as(bindManager::bindAssets);
    }

    @Nonnull
    @Override
    public Mono<Void> bindAssets(@NonNull String targetType,
                                 @NonNull String targetId,
                                 @NonNull String assetType,
                                 @NonNull Collection<?> assetId) {
        //没有配置自动绑定或者忽略处理的资产类型
        if (!autoBinding || isExclude(assetType)) {
            return Mono.empty();
        }
        return Flux
            .fromIterable(dimensions)
            .filter(dimension -> dimension.typeIs(targetType) && Objects.equals(targetId, dimension.getId()))
            //绑定到全部维度
            .map(dimension -> createAssetBindRequest(dimension, assetType, assetId))
            .as(bindManager::bindAssets);
    }

    @Nonnull
    @Override
    public Mono<Void> bindAssets(@NonNull String assetType,
                                 @NonNull Collection<?> assetId) {
        //没有配置自动绑定或者忽略处理的资产类型
        if (!autoBinding || isExclude(assetType)) {
            return Mono.empty();
        }
        return Flux
            .fromIterable(dimensions)
            //绑定到全部维度
            .map(dimension -> createAssetBindRequest(dimension, assetType, assetId))
            .as(bindManager::bindAssets);
    }

    @Nonnull
    @Override
    public Mono<Void> unbindAssets(@NonNull String assetType,
                                   @NonNull Collection<?> assetId) {
        //没有配置自动绑定或者忽略处理的资产类型
        if (!autoBinding || isExclude(assetType)) {
            return Mono.empty();
        }
        return Flux
            .fromIterable(dimensions)
            //绑定到全部维度
            .map(dimension -> createAssetUnbindRequest(dimension, assetType, assetId))
            .as(bindManager::unbindAssets);
    }

    @Nonnull
    @Override
    public Mono<Void> unbindAssets(@NonNull String targetType,
                                   @NonNull String targetId,
                                   @NonNull String assetType,
                                   @NonNull Collection<?> assetId) {
        //没有配置自动绑定或者忽略处理的资产类型
        if (!autoBinding || isExclude(assetType)) {
            return Mono.empty();
        }
        return Flux
            .fromIterable(dimensions)
            //只处理支持的维度
            .filter(dimension -> dimension.typeIs(targetType) && Objects.equals(targetId, dimension.getId()))
            //创建绑定请求
            .map(dimension -> createAssetUnbindRequest(dimension, assetType, assetId))
            //进行绑定
            .as(bindManager::unbindAssets);
    }


    private AssetBindRequest createAssetBindRequest(Dimension dimension, String assetType, Collection<?> assetId) {
        AssetBindRequest request = new AssetBindRequest();
        //资产类型
        request.setAssetType(assetType);
        //资产ID
        request.setAssetIdList(assetId.stream().map(String::valueOf).collect(Collectors.toList()));
        //维度类型,如: 公司,部门
        request.setTargetType(dimension.getType().getId());
        //维度ID,如: 公司ID,部门ID
        request.setTargetId(dimension.getId());
        //默认关系owner
        request.setRelation(AssetRelation.owner);
        //默认全部权限
        request.setAllPermission(true);
        return request;
    }

    private AssetUnbindRequest createAssetUnbindRequest(Dimension dimension, String assetType, Collection<?> assetId) {
        AssetUnbindRequest request = new AssetUnbindRequest();
        //资产类型
        request.setAssetType(assetType);
        //资产ID
        request.setAssetIdList(assetId.stream().map(String::valueOf).collect(Collectors.toList()));
        //维度类型
        request.setTargetType(dimension.getType().getId());
        //维度ID
        request.setTargetId(dimension.getId());
        return request;
    }

    @Override
    public @NonNull Mono<Boolean> hasPermission(@NonNull String assetType,
                                                @NonNull Collection<?> assetId,
                                                boolean allowAssetNotExist,
                                                @NonNull AssetPermission... permission) {

        if (CollectionUtils.isEmpty(assetId)) {
            return Mono.just(true);
        }
        //返回empty,表示此Holder不处理此资产类型,交给调用者判断是否有权限.
        if (isExclude(assetType)) {
            return Mono.empty();
        }
        AssetType type = AssetType.of(assetType);
        //没有匹配到的资产ID,如果不为空,说明可能没有这些资产的权限
        Set<?> nonMatch = new HashSet<>(assetId);
        return this
            .getBindings(type, assetId)
            //按资产ID分组,处理多个维度分配了同一个资产,但是资产权限可能不同的情况
            .groupBy(AssetBinding::getAssetId)
            .flatMap(group -> group
                //多个资产绑定合并在一起进行判断
                .reduce(AssetBinding::merge)
                .map(asset -> {
                    //已经匹配过了,移除之
                    nonMatch.remove(asset.getAssetId());
                    return asset.hasPermission(permission);
                })
            )
            //全部都匹配了说明资产已经绑定.
            .filter(ignore -> nonMatch.isEmpty())
            //否则说明有资产没有绑定,
            .switchIfEmpty(Mono.defer(() -> {
                //资产不存在时允许操作
                if (allowAssetNotExist) {
                    return this
                        .assetManager
                        .getAssets(type, nonMatch)
                        .hasElements()
                        //数据存在,不能进行操作
                        .map(exist -> !exist);
                }
                return Mono.just(false);
            }))
            //全部通过才认为有权限
            .all(Boolean::booleanValue);
    }

    @Override
    public @NonNull <T> Flux<T> filter(@NonNull Flux<T> source,
                                       @NonNull AssetType assetType,
                                       @NonNull Function<T, ?> assetIdMapper,
                                       @NonNull AssetPermission... permission) {
        if (isExclude(assetType)) {
            return source;
        }
        return source
            //每200个批量进行处理
            .buffer(200)
            .flatMap(list -> {
                //缓存 Key为资产ID,value为资产对象
                Map<?, T> cache = list.stream().collect(Collectors.toMap(assetIdMapper, Function.identity()));
                //有权限的资产缓存
                Map<String, T> passed = new ConcurrentHashMap<>();
                return this
                    .getBindings(assetType, cache.keySet())
                    //过滤掉没有权限的资产
                    .filter(asset -> asset.hasPermission(permission))
                    //将有权限的资产放入缓存
                    .doOnNext(asset -> passed.put(asset.getAssetId(), cache.get(asset.getAssetId())))
                    //返回缓存,使用Flux.defer,因为要等上游结束了,passed才能填充完毕.
                    .thenMany(Flux.defer(() -> Flux.fromIterable(passed.values())));
            });
    }

    @Override
    public <T extends Conditional<T>> NestConditional<T> injectConditional(NestConditional<T> conditional,
                                                                           String assetType,
                                                                           String idProperty) {
        if (isExclude(assetType)) {
            return conditional;
        }
        //注入嵌套的资产权限查询条件:
        // exists(select 1 from s_dimension_assets_bind _bind
        // where _bind.asset_type =?
        //      and t.id = _bind.asset_id
        //      and _bind.target_key in(?,?)
        // )
        return conditional
            .accept(idProperty,
                    DimensionAssetsTermBuilder.termType,
                    DimensionAssetsTerm.of(dimensions, assetType, false));
    }

    @Override
    public int getOrder() {
        return ORDER_DEFAULT;
    }

    @AllArgsConstructor
    @Generated
    static class SimpleDimensionBinding implements DimensionBinding {

        private final Dimension dimension;

        @Override
        public DimensionType getTargetType() {
            return dimension.getType();
        }

        @Override
        public String getTargetId() {
            return dimension.getId();
        }

        @Override
        public int getPriority() {
            return ORDER_DEFAULT;
        }
    }
}
